#region imported Namespaces
using System;
using System.Collections;
using System.Data;
using System.Data.SqlClient;
using System.Text;

using BO = PHSRAG.Insight21.BusinessObjects;
using DataAccess = PHSRAG.Utility.DataAccess;
using ICachePolicy = PHSRAG.Insight21.Policy.ICachePolicy;
using SearchCriteria = PHSRAG.Insight21.SearchContext.SearchCriteria;
#endregion

namespace PHSRAG.Insight21.Personnel
{
	#region Class PayrollExceptions
	/// <summary>
	/// The PayrollExceptions object supports loading of exception information
	/// for all personnel to which the user has access. It also exposes methods to 
	/// support sorting and searching on the retrieved data.
	/// </summary>
	public class PayrollExceptions:BO.BusinessObject
	{
		#region Constants
		private const string	PayrollExceptionsKey				= "PayrollExceptions_D9B7DEF7-A32F-4C11-ABA9-8AF389AE5F9C";
		private const string	DefaultSortColumn					= "NewGroupEffectiveDate";
		private const int		DomainUserIdArgumentIndex			= 0;
		private const int		ReportDateArgumentIndex				= 1;
		private const int		ExceptionReportFlagArgumentIndex	= 2;
		private const int		ExceptionsDataTableIndex			= 0;
		private const string	ExceptionMessageColumnName			= "Exception";
		private const bool ExceptionReportFlag =true;
		// Generate Exception message constants	
		private const string JobMapIdColumnName							= "EmployeeJobMapId";
		private const string EffectiveDateColumnName					= "NewGroupEffectiveDate";
		private const string ExpirationDateColumnName					= "NewStackEndDate";
		private const string NewTotalAnnualRateColumnName				= "NewTotalAnnualRate";
		private const string AllocationPercentErrorFlagColumnName		= "AllocationPercentErrorFlag";
		private const string GrantStatusColumnName						= "GrantStatus";
		private const string NewAllocatedAnnualRateColumnName			= "NewAllocatedAnnualRate";
		private const string NewFundNumberColumnName					= "NewFundNumber";
		private const string ChangedBottomOfStackErrorFlagColumnName	= "ChangedBottomOfStackErrorFlag";
		private const string GrantStatusActiveType						= "A";
		private const string GrantStatusInactiveType					= "I";
		// Constants to hold the Exception Messages
		private const string AllocationPendingFund		= "Allocated Pending Fund";
		private const string AllocatedInactiveFund		= "Allocated Inactive Fund";
		private const string SalaryNotAllocated			= "Salary not allocated 100%";
		private const string ZeroSalaryDistribution		= "Zero Salary Distribution amount";
		private const string SalaryOverAllocated		= "Salary Over Allocated";
		private const string LikeOperator				= " Like";
		#endregion

		#region Instance Variables
		private Hashtable		payrollExceptionMap		= new Hashtable();
		private string			sortColumn				= DefaultSortColumn;
		private string			currentDataTableName	= Constants.OriginalDataTableName;
		private bool			sortAscending			= true;
		#endregion

		#region Constructor
		/// <summary>
		/// Constructor, also intialize the base class with the prevalent cache policy
		/// </summary>
		/// <param name="cachePolicy">The prevalent cache policy</param>
		public PayrollExceptions() : base(null)
		{
		}
		#endregion

		#region Public Properties
		/// <summary>
		/// Get the key to be used in all caching operations for this instance of the object.
		/// </summary>
		public static string CacheKey	{ get { return PayrollExceptionsKey; } }
		/// <summary>
		/// Get the total number of exception rows in the data container.
		/// Returns 0 if there is no data in the data container
		/// </summary>
		public int Count
		{
			get { return (DataContainer.Tables.Count == 0) ? 0 : DataContainer.Tables[currentDataTableName].Rows.Count; }
		}
		/// <summary>
		/// Get the sort sequence.
		/// </summary>
		public bool SortAscending
		{
			get { return sortAscending; }
		}

		/// <summary>
		/// Get the name of the sort column.
		/// </summary>
		public string SortColumn
		{
			get { return sortColumn; }
		}
		/// <summary>
		/// Get the table name of the active table in the data container
		/// This will be used for selective downloading of data into reports
		/// </summary>
		public string CurrentDataTableName
		{
			get { return currentDataTableName;}
		}
		#endregion

		#region BusinessObject Methods
		/// <summary>
		/// Load the payroll exception data for the current user and set the datatable name
		/// This method also generates the Exception Message.
		/// The method expects 2 paramters
		/// Domain User Id
		/// Report Date
		/// The ExceptionReportFlag is set to True which is required for the stored procedure.
		/// Check if the DataContainer has datatables otherwise throw an error.
		/// Exception column is added to the datatable which is used to display the exception messages.
		/// Sort the laoded data on the default sort column
		/// </summary>
		/// <param name="connectionString">Database connection string</param>
		/// <param name="args">Arguments required to load the exception data</param>
		public override void Load(string connectionString, params object[] args)
		{
			if (args.Length != 2)
				throw new Exception("Payroll exception load requires DomainUserID and ReportDate as parameters.");

			DataContainer =	DataAccess.GetDataSet(
				connectionString
				,Constants.GetPayrollReportSPROCName 
				,DataAccess.BuildSqlParameterArray(
					"@domainUserID",SqlDbType.Int,(int)args[DomainUserIdArgumentIndex]
					,"@reportDate",SqlDbType.VarChar,(string)args[ReportDateArgumentIndex]
					,"@exceptionReportFlag",SqlDbType.Bit,ExceptionReportFlag));

			if (DataContainer.Tables.Count ==1)
			{
				DataContainer.Tables[ExceptionsDataTableIndex].TableName = Constants.OriginalDataTableName;
				
				DataTable exceptionTable = DataContainer.Tables[Constants.OriginalDataTableName];
				exceptionTable.Columns.Add(ExceptionMessageColumnName,typeof(string));
					
				foreach(DataRow exceptionRow in exceptionTable.Rows)
						GenerateExceptionMessage(exceptionRow);

				exceptionTable.AcceptChanges();
			}
			else
				throw new Exception("No Exception data found for the user.");

			AddTableWithDistinctRows(DataContainer.Tables[Constants.OriginalDataTableName]);
		}
		#endregion

		#region Public Methods
		/// <summary>
		/// Get the list of PayrollException objects bound by a start and end index. 
		/// These indeces are into the list of Payroll Exceptions that was loaded during the 
		/// Load() method execution into the data container. Typically, the start index is on 
		/// a "page" boundary, where the "page" referes to a datagrid page.
		/// A Hashtable of ArrayList objects is maintained using the start index as the key. 
		/// This allows retrieval of the set of PayrollException objects associated with that 
		/// index, thereby directly supporting random access based the page index.
		/// </summary>
		/// <param name="startIndex">Index within the Payroll Exception list to begin at</param>
		/// <param name="count">Total number of PayrollException objects to load</param>
		/// <returns>The list of PayrollException objects</returns>
		public ArrayList GetPayrollExceptionList(int startIndex, int count)
		{
			ArrayList payrollExceptionList = payrollExceptionMap[startIndex] as ArrayList;
			if (payrollExceptionList == null)
			{
				payrollExceptionList = new ArrayList();
				DataTable exceptionTable = DataContainer.Tables[currentDataTableName];
				for (int indexCount = startIndex, end = Math.Min(indexCount + count, exceptionTable.Rows.Count); indexCount < end; ++indexCount)
					payrollExceptionList.Add(new PayrollException(CachePolicy, exceptionTable.Rows[indexCount]));
				payrollExceptionMap[startIndex] = payrollExceptionList;
			}
			return payrollExceptionList;
		}
		/// <summary>
		/// Search the DataContainer for rows which match the specified search criteria.
		/// Two DataTables are maintained within the DataContainer; a full-complement DataTable and a filtered one.
		/// This allows filtering without having to hit the database. If a filter is in
		/// effect, we simply set the value of the currentTableName to Constants.FilteredDataTableName so that the
		/// GetPayrollExceptionList() method may retreive from the appropriate DataTable.
		/// </summary>
		/// <param name="searchCriteria">The search criteria instance picked from the search context</param>
		/// <remarks>True if the search criteria did any filtering; false otherwise</remarks>
		public bool Search(SearchCriteria searchCriteria)
		{
			payrollExceptionMap.Clear();

			StringBuilder filter = new StringBuilder();

			foreach (string key in searchCriteria.Keys)
			{
				switch (key.ToLower())
				{
					case "employeefirstname":
						filter.AppendFormat("{0} FirstName LIKE '{1}%'",(filter.Length>0)?"AND":"",searchCriteria[key]);
						break;
					case "employeelastname":
						filter.AppendFormat("{0} LastName LIKE '{1}%'",(filter.Length>0)?"AND":"",searchCriteria[key]);
						break;
					case "exceptionmessage":
						filter.AppendFormat("{0} Exception LIKE '{1}%'"
							,(filter.Length>0)?"AND":""
							,(searchCriteria[key] == "All")? string.Empty : searchCriteria[key]);
						break;
					case "pi":
						filter.AppendFormat("{0} PIID ='{1}'",(filter.Length>0)?"AND":"",searchCriteria[key]);
						break;
				}
			}
			
			bool applySearch = (filter.Length > 0);
			DataTable exceptionTable = DataContainer.Tables[Constants.OriginalDataTableName];
			if(applySearch)
			{
				DataTable filteredDataTable = exceptionTable.Clone();
				filteredDataTable.TableName = currentDataTableName = Constants.FilteredDataTableName;

				DataRow[] filteredRows = exceptionTable.Select(filter.ToString(),JobMapIdColumnName);
				foreach (DataRow exceptioRow in filteredRows)
					filteredDataTable.ImportRow(exceptioRow);

				AddTableWithDistinctRows(filteredDataTable);
			}
			else
				AddTableWithDistinctRows(exceptionTable);

			return applySearch;
		}
		/// <summary>
		/// Sort the list of payroll exceptions by the speicified column.
		/// If the column is to be sorted is the same as what was last sorted on, the sort sequence is toggled. Else, the
		/// sort sequence is set to ascending.
		/// </summary>
		/// <param name="columnName">Column to sort by</param>
		public void Sort(string columnName)
		{
			payrollExceptionMap.Clear();

			sortAscending = (sortColumn == columnName) ? !sortAscending : true;
			sortColumn = columnName;
			
			ProcessSort();
		}
		#endregion
		
		# region Private Methods
		/// <summary>
		/// GenerateExceptionMessage is used to dynamically create the Exception Message.
		/// If no exception message is generated, then the respective row is removed from the datatable as this row 
		/// will not be used for display in the grid otherwise the message is added to the 
		/// Exception column in the datarow instance.
		/// </summary>
		/// <param name="exceptionRow">Row for which the exception message is to be generated.</param>
		private void GenerateExceptionMessage(DataRow exceptionRow)
		{
			double totalAnnualRate = Convert.ToDouble(exceptionRow[NewTotalAnnualRateColumnName]);
			DateTime effectiveDate	= Convert.ToDateTime(exceptionRow[EffectiveDateColumnName]);
			string expirationDate = exceptionRow[ExpirationDateColumnName].ToString();
			string grantStatus = exceptionRow[GrantStatusColumnName].ToString();
			string newFundNumber = exceptionRow[NewFundNumberColumnName].ToString();
			double allocatedAnnualRate = Convert.ToDouble(exceptionRow[NewAllocatedAnnualRateColumnName]);

			bool allocationPercentErrorFlag = (exceptionRow[AllocationPercentErrorFlagColumnName]==System.DBNull.Value)?
				false:true;
			
			bool noDistributionFlags = (totalAnnualRate == 0 && allocationPercentErrorFlag);

			StringBuilder exceptionMessage = new StringBuilder();
			
            //if((grantStatus != GrantStatusActiveType) && (allocatedAnnualRate != 0))
            //    exceptionMessage.AppendFormat("{0}: {1}"
            //        ,(grantStatus!=GrantStatusInactiveType)?AllocationPendingFund:AllocatedInactiveFund
            //        ,newFundNumber);

			bool changedBottomOfStackErrorFlag=false;
			
			if(exceptionRow[ChangedBottomOfStackErrorFlagColumnName] != DBNull.Value)
				changedBottomOfStackErrorFlag = 
					Convert.ToBoolean(exceptionRow[ChangedBottomOfStackErrorFlagColumnName]);

			if(allocationPercentErrorFlag)
				if (!noDistributionFlags)
					exceptionMessage.AppendFormat("{0}: Salary: {1:c} Allocated Salary: {2:c}"
					,(totalAnnualRate < allocatedAnnualRate)?SalaryOverAllocated:SalaryNotAllocated
					,totalAnnualRate,allocatedAnnualRate);
				else
					exceptionMessage.AppendFormat(ZeroSalaryDistribution);

			if (exceptionMessage.ToString() == string.Empty)
				exceptionRow.Delete();
			else
				exceptionRow[ExceptionMessageColumnName] = exceptionMessage.ToString();
		}
		/// <summary>
		/// Sorts the current data table on the sortColumn instance variable
		/// in the order specified by the sortOrder instance variable
		/// </summary>
		private void ProcessSort()
		{
			DataTable exceptionTable = DataContainer.Tables[currentDataTableName];
			DataView exceptionView = exceptionTable.DefaultView;
			exceptionView.Sort = string.Format("{0} {1}", 
				sortColumn, sortAscending ? Constants.SortOrderAscending : Constants.SortOrderDescending);

			DataTable sortedDataTable = exceptionTable.Clone();
			foreach (DataRowView exceptionRowView in exceptionView)
				sortedDataTable.ImportRow(exceptionRowView.Row);

			if(DataContainer.Tables.Contains(currentDataTableName))
				DataContainer.Tables.Remove(currentDataTableName);
			
			DataContainer.Tables.Add(sortedDataTable);
		}
		/// <summary>
		/// Add a table with distinct rows to the data container
		/// </summary>
		/// <param name="summaryTable">datatable with payroll exception data,
		/// from which distinct rows will be picked</param>
		private void AddTableWithDistinctRows(DataTable exceptionTable)
		{
			DataTable distinctTable		= exceptionTable.Clone();
			distinctTable.TableName		= currentDataTableName = Constants.FilteredDataTableName;

			if(exceptionTable.Rows.Count >= 1)
			{
				foreach (DataRow exceptionRow in exceptionTable.Rows)
				{
					// If a row with the same Job map ID, exception message, effective
					// and expiration date is in the distinct table, ignore row, otherwise import
					if (distinctTable.Select(string.Format("{0} = '{1}' AND {2} = '{3}' AND {4} = '{5}' AND {6} = '{7}'",
						JobMapIdColumnName,
						exceptionRow[JobMapIdColumnName],
						ExceptionMessageColumnName,
						exceptionRow[ExceptionMessageColumnName],
						EffectiveDateColumnName,
						exceptionRow[EffectiveDateColumnName],
						ExpirationDateColumnName,
						exceptionRow[ExpirationDateColumnName])).Length == 0)
					{
						distinctTable.ImportRow(exceptionRow);
					}
				}
			}						
			if(DataContainer.Tables.Contains(currentDataTableName))
				DataContainer.Tables.Remove(currentDataTableName);
			DataContainer.Tables.Add(distinctTable);

			ProcessSort();
		}
		#endregion
	}
	#endregion
}